/* 
 *  Copyright 2012 CodeMagi, Inc.
 * 
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */
package com.codemagi.servlets.content;

import com.codemagi.login.AccessException;
import com.codemagi.login.LoginException;
import com.codemagi.login.model.IUser;
import com.codemagi.servlets.*;
import com.codemagi.servlets.content.thumbnailers.IThumbnailer;
import com.codemagi.servlets.upload.model.Upload;
import com.codemagi.util.*;
import com.sun.media.jai.codec.FileSeekableStream;
import java.awt.*;
import java.awt.color.*;
import java.awt.image.*;
import java.awt.image.renderable.*;
import java.io.*;
import java.net.URL;
import java.util.*;
import javax.media.jai.*;
import javax.servlet.*;
import javax.servlet.http.*;
import javax.swing.ImageIcon;
import org.apache.log4j.*;
import org.exolab.castor.jdo.Database;
import org.exolab.castor.jdo.PersistenceException;

/**                            
 * Class extending the <code>HttpServlet</code> to implement                                           
 * the controller for retrieving thumbnails of any size.
 *
 * This version uses the OLD URL scheme: /thumbnail/WIDTH/HEIGHT/ID
 *
 * @author August Detlefsen for CodeMagi, Inc.
 * @version 0.1 02/25/2005
 */
public class ThumbnailController extends com.codemagi.servlets.BaseServlet {

    //set to true to view debug output                                                                                       
    static Logger log = Logger.getLogger("com.codemagi.servlets.content.ThumbnailController");

    protected static final String ACCESS_ERROR = "You do not have access to this thumbnail.";

    //MESSAGES specific to thumbnailing
    public static final String MSG_THUMBNAIL_ERROR  = "Unable to create thumbnail";

    //MEMBERS
    public static String THUMBS_DIRECTORY  = "/thumbs";
    protected static String ICONS_DIRECTORY   = "/icons";

    public static float THUMBNAIL_ENCODE_QUALITY = 0.75f;
    public static float DOWNLOAD_ENCODE_QUALITY  = 1.0f;

    protected static String IMAGE_WATERMARK = ""; //"/WEB-INF/etc/watermark.png";

    protected Properties properties = new Properties();

    /** Location of the root context for writing thumbnails */
    protected static String ROOT_PATH = location;

    /**
     * HashMap of thumbnailers to use for different file extensions
     */
    protected static Hashtable thumbnailers = new Hashtable();
    static {
	thumbnailers.put("default", new com.codemagi.servlets.content.thumbnailers.JAIThumbnailer());
    }

    /**
     * Max dimension of a thumbnail before checking for Download object
     */
    protected static Integer SECURITY_THRESHOLD = 400; 

    /**
     * The class that will be used to determine the file info
     */
    protected Class UPLOAD_CLASS = com.codemagi.servlets.upload.model.Upload.class;

    /**
     * The name of the group that will be allowed admin functionality. Defaults to: SUPERUSER
     */
    protected static String ADMINISTRATOR_GROUP = "SUPERUSER";

    /**
     * Map of icons to be used for thumbnail placeholders
     */
    private static java.util.HashMap ICONS = new java.util.HashMap();

    //CONSTANTS
    protected static final ColorModel COLOR_MODEL_RGB;
   
    static {
	final int[] bits = { 8, 8, 8 };
	COLOR_MODEL_RGB = new ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_sRGB),
						 bits, false, false,
						 Transparency.OPAQUE,
						 DataBuffer.TYPE_BYTE);
    }

    public void init(ServletConfig config) throws ServletException {

	super.init(config);

	application.setAttribute("thumbnailController", this);
	
	log.debug("*** about to call init");	
	initIcons(config);

	//get the root app location to write files
	ServletContext rootContext = getServletContext().getContext("/");
	ROOT_PATH                  = rootContext.getRealPath("");

	//see if an upload class has been specified in the init params
	String uploadClass  = config.getInitParameter("uploadClass");
	if (!Utils.isEmpty(uploadClass)) {
	    try {
		UPLOAD_CLASS = Class.forName(uploadClass);
	    } catch (Exception e) {
		log.error("ERROR: Unable to instantiate uploadClass: " + uploadClass);
	    }
	}

	String pWatermark = config.getInitParameter("watermark.image");
	if ( !Utils.isEmpty(pWatermark) ) IMAGE_WATERMARK = pWatermark;

	//load the properties file and set views based on it
	String propFilename = config.getInitParameter("properties.filename");
	if (!Utils.isEmpty(propFilename)) {
	    InputStream in = null;

	    try {	    
		log.debug("Loading thumbnailer properties from: " + "/" + WEB_INF_DIR + ETC_DIR + propFilename);
		in = application.getResourceAsStream("/" + WEB_INF_DIR + ETC_DIR + propFilename);
		properties.load(in);
		
		//initialize the list of thumbnailer plugins
		Enumeration e = properties.propertyNames();
		while (e.hasMoreElements()) {
		    String name      = (String)e.nextElement();

		    //only bother with properties that start with thumbnailer
		    if (name.startsWith("thumbnailer.")) {
			String value = (String)properties.getProperty(name);
			log.debug("config name=" + name.substring(12) + " value=" + value);
			IThumbnailer currentThumbnailer = (IThumbnailer)Class.forName(value).newInstance();
			currentThumbnailer.setApplication(application);
			thumbnailers.put(StringUtils.toLowerCase(name.substring(12)), currentThumbnailer);
			log.debug("thumbnailers.put successful");
		    }
		}
		log.debug("total thumbnailers: " + thumbnailers.size());

	    } catch (IOException ioe) {
		log.debug("", ioe);
		throw new ServletException(MSG_PROPERTIES_NOT_FOUND);

	    } catch (ClassNotFoundException cnfe) {
		log.debug("", cnfe);
		throw new ServletException(MSG_PROPERTIES_ERROR);

	    } catch (InstantiationException ie) {
		log.debug("", ie);
                throw new ServletException(MSG_PROPERTIES_ERROR);

            } catch (IllegalAccessException ile) {
		log.debug("", ile);
                throw new ServletException(MSG_PROPERTIES_ERROR);

	    } catch (NullPointerException npe) {
		log.debug("", npe);
		throw new ServletException(MSG_PROPERTIES_ERROR);

	    } finally {
		try {
		    in.close();
		} catch (IOException ioe) {
		    log.debug("", ioe);
		}

	    }
	} else {
	    log.debug("thumbnailer properties file not found");
	}

	//place a reference to this servlet in the application context                             
        application.setAttribute(config.getServletName(), this);
    }

    /*
     * Loads the file name for thumbnail placeholder images.
     *
     * All files must be of the type file extension.jpg
     * ex/ photoshop file thumbnail replacement would be psd.jpg
     */

    /**
     *
     * @param config
     */
    
    public void initIcons(ServletConfig config) {

	log.debug("*** ThumbnailController initIcons called");

	java.io.File iconDir = null;

	String iconsDir = config.getInitParameter("iconsDirectory");
	log.debug("*** iconsDirectory = " + iconsDir);

	if (iconsDir != null) {
	    ICONS_DIRECTORY = iconsDir;
	} else {
	    log.debug("config error: iconsDirectory has not been set in web.xml - using default");
	}
 
        iconDir = new java.io.File(location + ICONS_DIRECTORY);
	log.debug("Attempting to load icons from: " + iconDir);

	if (!iconDir.isDirectory()) {
	    log.debug("config error: ICON_DIR is not a directory");
	    return;
	    //	    throw new RuntimeException("config error: ICON_DIR is not a directory!"); 
	}

	log.debug("icon config GOOD. dir=" + ICONS_DIRECTORY);

	String tempExtension;
	
	String[] iconList = iconDir.list(); 
	for (int i = 0; i < iconList.length; i++) {
	    if (iconList[i].endsWith(".jpg") || iconList[i].endsWith(".jpeg")) {
		tempExtension = "." + iconList[i].substring(0, iconList[i].indexOf("."));
		
		log.debug("placing " + tempExtension + " into icon HashMap");

		ICONS.put(tempExtension, iconList[i]);
	    }
	}
    }

    /**
     * Handles the incoming requests.
     * @param request
     * @param response
     * @throws javax.servlet.ServletException
     * @throws java.io.IOException
     */
    public void service(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException {

	//next page to forward to 
	String nextPage = VIEW_MAIN;

	//get the command passed in from the URL                           
        String command      = request.getPathInfo();

	try {
	    nextPage = dispatchCommand(request, response);

        } catch (AccessException ae) {
            log.debug("", ae);
            nextPage = VIEW_ACCESS_DENIED;

        } catch (LoginException le) {
            nextPage = VIEW_LOGIN;

        }

	if ( !VIEW_NONE.equals(nextPage)) {
	    //forward on                                                                                           
	    log.debug("PageController FORWARDING TO: " + nextPage);
	    RequestDispatcher dispatcher = getServletContext().getContext("/").getRequestDispatcher(nextPage);
	    dispatcher.forward(request, response);
	}

	return;
    }


    //DISPATCHERS ----------------------------------------------------------------------      

    /**
     *
     * @param request
     * @param response
     * @return
     * @throws LoginException
     * @throws AccessException
     * @throws ServletException
     */
    
    protected String dispatchCommand(HttpServletRequest request, HttpServletResponse response) 
	throws LoginException, AccessException, ServletException {

        String nextPage = VIEW_MAIN;

	OutputStream out = null;

        try {
            //get the asset ID, width and height from the URL  
            String command  = request.getPathInfo();
            command         = command.substring(1);  //trim the leading slash                

	    boolean doOverlay = true;	    
	    String[] parsedCommands = command.split("/", 3);
            Integer width;  
            Integer height; 
            Integer imageId = null;
	    String sourceLocation = null;

	    try {
		width   = convertInteger( parsedCommands[0] );
		height  = convertInteger( parsedCommands[1] );
		try {
		    imageId = new Integer( parsedCommands[2] );
		} catch (NumberFormatException nfe) {
		    sourceLocation = parsedCommands[2];
		}
	    } catch (ArrayIndexOutOfBoundsException aioobe) {
		throw new AppException("dispatchCommand called without enough parameters");
	    }

            IUser user = getUser(request);
	    //determine the source image from either a loaded asset or a passed-in path
	    if (imageId != null) {
		//load the specified asset from the database by ID
		Upload asset = getAsset(imageId);
		if (!asset.hasUser(user)){
		    throw new AccessException(ACCESS_ERROR);
		}

		//check if asset does not need watermark (according to DB)
		if ( Utils.isEmpty(IMAGE_WATERMARK)  || (!asset.isWatermark())) {

		    doOverlay = false;
		}

		//check to make sure people do not try to d/l really big thumbnails unless they are the administrator
		if ( (height > SECURITY_THRESHOLD || width > SECURITY_THRESHOLD) && 
		     !user.isInGroup(ADMINISTRATOR_GROUP) ) {

		    throw new AccessException("Maximum thumbnail size threshold exceeded");
		}

		sourceLocation = asset.getLink();

	    } else {
		sourceLocation = "/" + sourceLocation;

		if (!user.isInGroup(ADMINISTRATOR_GROUP)) throw new AccessException(ACCESS_ERROR);
	    }

	    log.debug("sourceLocation: " + sourceLocation);

	    String extension = sourceLocation.substring(sourceLocation.lastIndexOf(".")+1).toLowerCase();
	    log.debug("extension: " + extension);

	    //determine the location and filename for the new thumbnail
	    String thumbLocation = getThumbLocation(sourceLocation, width, height, doOverlay);
	    log.debug("thumbLocation: " + thumbLocation);

	    //if the file doesnt exist already create a new thumbnail
	    File thumbFile  = new File(ROOT_PATH + thumbLocation);
	    File sourceFile = new File(ROOT_PATH + sourceLocation);

	    if (!thumbFile.exists() || thumbFile.lastModified() < sourceFile.lastModified() ) {
		try {		   
		    createThumbnail( ROOT_PATH + sourceLocation, ROOT_PATH + thumbLocation, width.intValue(), height.intValue(), doOverlay);
		} catch (Exception e) {
		    //if createThumbnail fails for any reason, just use a "canned thumbnail"
		    log.debug("Thumbnailing Error", e);
		    createPlaceholderThumbnail( ROOT_PATH + sourceLocation, ROOT_PATH + thumbLocation, width, height );
		}
	    }
	    
	    //stream the image back to the requestor
	    nextPage = thumbLocation;

	} catch (AppException ae) {
	    log.debug("", ae);

	} catch (PersistenceException pe) {
	    log.debug("", pe);

	} finally {
	    closeStream(out);
	}

	return nextPage;
    }

    /**
     * Gets a thumbnail without doing any security checking! (Useful for Lightbox)
     * @param imageId
     * @param width
     * @param height
     * @return 
     */
    protected byte[] getThumbnail(Integer imageId, int width, int height) {

	boolean doOverlay = true;
	String sourceFile = null;
	byte[] thumbBytes = null;	
	
        try {
	    if (imageId != null) {
		//load the specified asset from the database by ID
		Upload asset = getAsset(imageId);

		//check if asset should get a watermark
		if ( Utils.isEmpty(IMAGE_WATERMARK) ) {
		    doOverlay = false;
		}

		sourceFile = asset.getLink();
		log.debug("sourceFile: " + sourceFile);
	    }	

	    //determine the location and filename for the new thumbnail
	    String thumbLocation = getThumbLocation(sourceFile, width, height, doOverlay);
	    log.debug("thumbLocation: " + thumbLocation);

	    //if the file doesnt exist already create a new thumbnail
	    File thumbFile = new File(ROOT_PATH + thumbLocation);

	    if (!thumbFile.exists()) {
		log.debug("createThumbnail 2");
		try {
		    createThumbnail( ROOT_PATH + sourceFile, ROOT_PATH + thumbLocation, width, height, doOverlay);
		} catch (Exception e) {
		    //if createThumbnail fails for any reason, just use a "canned thumbnail"
		    createPlaceholderThumbnail( ROOT_PATH + sourceFile, ROOT_PATH + thumbLocation, width, height );
		}
	    }
	    
	    //stream the image back to the requestor
	    thumbBytes = FileUtils.getFileAsBytes( thumbFile );

	} catch (AppException ae) {
	    log.debug("", ae);
	    
	} catch (PersistenceException pe) {
	    log.debug("", pe);
	    
	} catch (IOException ioe) {
	    log.debug("", ioe);
	    
	} 
	
	return thumbBytes;
    }


    /**
     * Gets a thumbnail without doing any security checking! (Useful for Lightbox)
     * @param imageId
     * @param width
     * @param height
     * @param doOverlay
     * @return 
     */
    protected byte[] getThumbnail(Integer imageId, int width, int height, boolean doOverlay) {

	String sourceFile = null;
	byte[] thumbBytes = null;	
	
        try {
	    if (imageId != null) {
		//load the specified asset from the database by ID
		Upload asset = getAsset(imageId);

		sourceFile = asset.getLink();
		log.debug("sourceFile: " + sourceFile);
	    }	

	    //determine the location and filename for the new thumbnail
	    String thumbLocation = getThumbLocation(sourceFile, width, height, doOverlay);
	    log.debug("thumbLocation: " + thumbLocation);

	    //if the file doesnt exist already create a new thumbnail
	    File thumbFile = new File(ROOT_PATH + thumbLocation);

	    if (!thumbFile.exists()) {
		log.debug("createThumbnail 3");
		try {
		    createThumbnail( ROOT_PATH + sourceFile, ROOT_PATH + thumbLocation, width, height, doOverlay);
		} catch (Exception e) {
		    //if createThumbnail fails for any reason, just use a "canned thumbnail"
		    createPlaceholderThumbnail( ROOT_PATH + sourceFile, ROOT_PATH + thumbLocation, width, height );
                }
	    }

	    //stream the image back to the requestor
	    thumbBytes = FileUtils.getFileAsBytes( thumbFile );

	} catch (AppException ae) {
	    log.debug("", ae);
	    
	} catch (PersistenceException pe) {
	    log.debug("", pe);
	    
	} catch (IOException ioe) {
	    log.debug("", ioe);
	    
	} 
	
	return thumbBytes;
    }

    /***
     * Gets a thumbnail without doing any security checking! (Useful for Lightbox)
     *
     * NOTE: Be sure to close this InputStream when you are done with it!
     *
     * @param imageId
     * @param width
     * @param height
     * @param doOverlay
     * @return InputStream
     */
    protected InputStream getThumbnailStream(Integer imageId, int width, int height, boolean doOverlay) {

	String sourceFile = null;

	InputStream thumbStream = null; 

        try {
	    if (imageId != null) {
		//load the specified asset from the database by ID
		Upload asset = getAsset(imageId);

		sourceFile = location + asset.getLink();
	    }	

	    
	    //determine the location and filename for the new thumbnail
	    String thumbLocation = getThumbLocation(sourceFile, width, height, doOverlay);
	    log.debug("sourceFile: " + sourceFile + " thumbLocation: " + thumbLocation);

	    //if the file doesnt exist already create a new thumbnail
	    //	    File thumbFile = new File(location + thumbLocation);
	    File thumbFile = new File(ROOT_PATH + thumbLocation);
	    
	    if (!thumbFile.exists()) {
		log.debug("createThumbnail 4");
		try {
		    //		    createThumbnail( location + sourceFile, location + thumbLocation, width, height, doOverlay);
		    createThumbnail( ROOT_PATH + sourceFile, ROOT_PATH + thumbLocation, width, height, doOverlay);
		} catch (AppException e) {
                    //if createThumbnail fails for any reason, just use a "canned thumbnail"
		    //                    createPlaceholderThumbnail( location + sourceFile, location + thumbLocation, width, height );
                    createPlaceholderThumbnail( ROOT_PATH + sourceFile, ROOT_PATH + thumbLocation, width, height );
                }
            }
	    
	    //stream the image back to the requestor
	    thumbStream = new FileInputStream( thumbFile );

	} catch (AppException ae) {
	    log.debug("", ae);
	    
	} catch (PersistenceException pe) {
	    log.debug("", pe);
	    
	} catch (IOException ioe) {
	    log.debug("", ioe);
	    
	} 
	
	return thumbStream;
    }

    /**
     *
     * @param image
     * @param edge
     * @param edgeColor
     * @return
     */
    public static RenderedOp border(RenderedOp image, int edge, double edgeColor) {

	ParameterBlock params = new ParameterBlock();
	params.addSource(image);
	params.add(edge);//left pad
	params.add(edge);//right pad
	params.add(edge);//top pad
	params.add(edge);//bottom pad
	double fill[] = {edgeColor};
	params.add(new BorderExtenderConstant(fill));//type
	params.add(edgeColor);//fill color
	return JAI.create("border", params);
    }

    /**                                                                                                                  
     * Creates a thumnail image from a source file
     *                                                                                                                
     * @param sourceFile     Full path to the source image                   
     * @param thumbFile      Full path to output the thumbnail file                                                 
     * @param targetWidth    Target maximum width for the thumbnail
     * @param targetHeight   Target maximum height for the thumbnail
     * @param doOverlay
     * @throws com.codemagi.servlets.AppException
     */
    protected synchronized void createThumbnail(String sourceFile, String thumbFile, int targetWidth, 
				   int targetHeight, boolean doOverlay)
        throws AppException {

	try {
	    
	    String extension = sourceFile.substring(sourceFile.lastIndexOf(".")+1);
	    
	    log.debug("createThumbnail checkpoint one - loading extension " + extension);
	    
	    IThumbnailer nailer = getThumbnailer(extension);
	    if (nailer == null) throw new AppException("nailer for extension " + extension + " failed");
	    
	    log.debug("createThumbnail checkpoint two - attempting thumbnail creation: " + thumbFile);
	    
	    nailer.createThumbnail( sourceFile, thumbFile, targetWidth, targetHeight, doOverlay);
	    
	    log.debug("createThumbnail checkpoint three - thumbnail creation complete");
	    
        } catch (Exception e) {
            log.debug("", e);
            throw new AppException(MSG_THUMBNAIL_ERROR);
        } catch (Throwable t) {
            log.debug("", t);
            throw new AppException(MSG_THUMBNAIL_ERROR);

        } 
    }

    /**
     * Creates a simple dynamically created thumbnail placeholder when createThumbnail fails
     *
     * @param sourceFile     Full path to the source image
     * @param thumbFile      Full path to output the thumbnail file
     * @param targetWidth    Target maximum width for the thumbnail
     * @param targetHeight   Target maximum height for the thumbnail
     * @throws com.codemagi.servlets.AppException
     */
    protected synchronized void createPlaceholderThumbnail(String sourceFile, String thumbFile, 
							   int targetWidth,
							   int targetHeight)
        throws AppException {

        String thumbDir = sourceFile.substring(0, sourceFile.lastIndexOf("/") + 1) + THUMBS_DIRECTORY + "/" ;

        log.debug("**** createPlaceholderThumbnail(" + sourceFile + ", " + thumbFile + ")");

        String extension = sourceFile.substring(sourceFile.lastIndexOf("."));

        try {
	    String icon = null;
	    
	    if (ICONS.get(extension) != null) icon = (String)ICONS.get(extension);
		    
	    log.debug("*** match text: " + icon);
	    BufferedImage thumbBi = null;
	    
	    if (icon != null) {

		String path = location + ICONS_DIRECTORY + java.io.File.separator + icon;
		log.debug("path = " + path);
		
		FileSeekableStream ss = new FileSeekableStream(path);
                ParameterBlock loadParams = new ParameterBlock();
                loadParams.add(ss);
                RenderedOp overlayOp  = JAI.create("jpeg", loadParams);
		// scale the watermark to the appropriate size
                overlayOp = scaleImage(overlayOp, targetWidth, targetHeight);
                overlayOp = cropRightAndBottom(overlayOp, 1, targetWidth, targetHeight);
		thumbBi = overlayOp.getAsBufferedImage();

	    } else {
		
		thumbBi = new BufferedImage(targetWidth, targetHeight, BufferedImage.TYPE_INT_RGB );
		Graphics2D g2d = thumbBi.createGraphics();
		g2d.setFont(new Font("ARIAL", Font.BOLD, 26));
		if (targetHeight>0) g2d.drawString(extension, (targetWidth/2)-24, targetHeight/2+3);
	    }
	    
            //create the output directory if needed
            java.io.File outDir = new java.io.File(thumbDir);
            outDir.mkdirs();

            com.sun.media.jai.codec.JPEGEncodeParam encodeParam = new com.sun.media.jai.codec.JPEGEncodeParam();
            encodeParam.setQuality(0.75f);
            //if (!doOverlay) encodeParam.setQuality(DOWNLOAD_ENCODE_QUALITY);

            //save the image to the filesystem
	    log.debug("Attempting to create thumbFile: " + thumbFile);
            JAI.create("filestore", thumbBi, thumbFile, "jpeg", encodeParam);
        } catch (Exception e) {
            log.debug("", e);
            throw new AppException(MSG_THUMBNAIL_ERROR);
        } catch (Throwable t) {
            log.debug("", t);
            throw new AppException(MSG_THUMBNAIL_ERROR);
        } 
    }

    /**
     * Crops the right and bottom sides on images only, this function was created cause of a one pixel
     * black line discovered when creating icon thumbnails around 100 pixels. All icons get this function
     * for good measure.
     *
     * @param image
     * @param edge Number of pixels to crop
     * @param width Width of the image argument
     * @param height
     * @return RenderedOp
     */
    public RenderedOp cropRightAndBottom(RenderedOp image, float edge, float width, float height)
    {
	try {
	    ParameterBlock params = new ParameterBlock();
	    params.addSource(image);
	    params.add((float)0);//x origin
	    params.add((float)0);//y origin
	    params.add((float) width - edge);//width
	    params.add((float) height - edge);//height
	    return JAI.create("crop", params); 
	} catch (Exception e) {
	    log.debug("Error trying to crop edge from thumbnail, ignoring attempt and continuing forward [edge="+ edge + ", width="+width+", height="+height+"]", e);
	}
	return image;
    }

    /** 
     * Determines the proper file name for a thumbnail based on the source file, width, height and doOverlay 
     * @param sourceFile
     * @param width
     * @param height
     * @param doOverlay
     * @return String
     */
    public String getThumbLocation(String sourceFile, int width, int height, boolean doOverlay) {

	String dir        = sourceFile.substring(0, sourceFile.lastIndexOf("/") + 1) + THUMBS_DIRECTORY + "/" ;
	String filename   = sourceFile.substring(sourceFile.lastIndexOf("/") + 1); 

	StringBuilder thumbLink = new StringBuilder(256);
	thumbLink
	    .append( dir)
	    .append( filename)
	    .append(".")
	    .append( width )
	    .append("x")
	    .append( height );
	
	if (!doOverlay) thumbLink.append(".no");
	
	thumbLink.append(".jpg");
	
	return thumbLink.toString();
    }


    /** 
     * Determines the proper path to forward to a thumbnail based on the source file, width, height and doOverlay 
     * @param sourceFile
     * @param width
     * @param height
     * @param doOverlay
     * @return String
     */
    public String getThumbPath(String sourceFile, int width, int height, boolean doOverlay) {

	String dir        = sourceFile.substring(0, sourceFile.lastIndexOf("/") + 1) + THUMBS_DIRECTORY + "/" ;
	String filename   = sourceFile.substring(sourceFile.lastIndexOf("/") + 1); 

	StringBuilder thumbLink = new StringBuilder(256);
	thumbLink
	    .append( dir)
	    .append( filename)
	    .append(".")
	    .append( width )
	    .append("x")
	    .append( height );
	
	if (!doOverlay) thumbLink.append(".no");
	
	thumbLink.append(".jpg");
	
	return thumbLink.toString();
    }

    /**
     * Returns the thumbnailer to use for the requested Upload
     * @param upload
     * @return IThumbnailer
     */
    public IThumbnailer getThumbnailer(Upload upload) {
	//sanity check
	if (upload == null) return null;

	String sourceFile = upload.getLink();
	if (Utils.isEmpty(sourceFile)) return null;

	String extension  = sourceFile.substring(sourceFile.lastIndexOf(".")+1);

	return getThumbnailer(extension);
    }

    /**
     * Returns the thumbnailer to use for files with the requested extension
     * @param fileExtension
     * @return IThumbnailer
     */
    public IThumbnailer getThumbnailer(String fileExtension) {
	fileExtension = StringUtils.toLowerCase(fileExtension);

	IThumbnailer output = (IThumbnailer)thumbnailers.get(fileExtension);

	if (output == null) output = (IThumbnailer)thumbnailers.get("default");

	return output;
    }


    //UTILITY METHODS ----------------------------------------------------

    /**
     *
     * @param id
     * @return
     * @throws PersistenceException
     */
    
    protected Upload getAsset(Integer id) throws PersistenceException {

	Database db = null;

	Upload output = null;

        try {
	    db = getJDODatabase();
	    db.begin();

	    log.debug("UPLOAD CLASS: " + UPLOAD_CLASS);

	    output = (Upload)db.load(UPLOAD_CLASS, id, Database.ReadOnly);

	} finally {
	    closeJDODatabase(db);

	}

	return output;
    }


    //IMAGE PROCESSING METHODS -------------------------------------------

    /**
     *
     * @param source
     * @param targetWidth
     * @param targetHeight
     * @return
     */
    
    public static RenderedOp scaleImage(RenderedOp source, int targetWidth, int targetHeight) {

	log.debug("scaleImage: " + source + ", tw: " + targetWidth + " th: " + targetHeight);

	// determine thumbnail size from WIDTH and HEIGHT                                                         
	float thumbWidth    = (float)targetWidth;
	float thumbHeight   = (float)targetHeight;
	double thumbRatio   = (double)targetWidth / (double)targetHeight;
	int imageWidth      = source.getWidth();
	int imageHeight     = source.getHeight();
	double imageRatio   = (double)imageWidth / (double)imageHeight;
	double scaleFactor;
	
	if (thumbRatio < imageRatio) {
	    thumbHeight = (int)(thumbWidth / imageRatio);
	    scaleFactor = (double)(thumbHeight / imageHeight);
	} else {
	    thumbWidth  = (int)(thumbHeight * imageRatio);
	    scaleFactor = (double)(thumbWidth / imageWidth);
	}
	log.debug("THUMB WIDTH: " + thumbWidth + " THUMB HEIGHT: " + thumbHeight + " SCALE FACTOR: " + scaleFactor);
	
	if (scaleFactor >= 1) return source;

	RenderingHints hints = new RenderingHints(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
	return JAI.create("SubsampleAverage", source, scaleFactor, scaleFactor, hints);
    }

    /**
     *
     * @param source
     * @return
     */
    public static RenderedOp coerceRgb(RenderedOp source) {

	//create a three banded sample model
	SampleModel sm = RasterFactory.createBandedSampleModel(DataBuffer.TYPE_BYTE,  
							       source.getWidth(),  
							       source.getHeight(),  
							       3); // three bands

	ImageLayout layout = new ImageLayout();
	layout.setColorModel(COLOR_MODEL_RGB);
	layout.setSampleModel(sm);

	HashMap map = new HashMap();  
	map.put(JAI.KEY_IMAGE_LAYOUT,layout);  
	RenderingHints hints = new RenderingHints(map);  

	// Reformat the image using the above hints.  
	ParameterBlock pb = new ParameterBlock();  
	pb.addSource(source);  

	return JAI.create("format",pb,hints);  
    }

    /**
     *
     * @param source
     * @param targetWidth
     * @param targetHeight
     * @return
     */
    public static RenderedOp overlayWatermark(RenderedOp source, int targetWidth, int targetHeight) {
	
	log.debug("overlayWatermark: " + source);

	Graphics2D g = null;
	
	FileSeekableStream ss = null;

	try {
	    //load the watermark image 
	    URL watermarkUrl     = application.getResource(IMAGE_WATERMARK);
	    RenderedOp overlayOp = JAI.create("url", watermarkUrl);
	    /*
	    ss                        = new FileSeekableStream(location + IMAGE_WATERMARK);
	    ParameterBlock loadParams = new ParameterBlock();
	    loadParams.add(ss);
	    RenderedOp overlayOp  = JAI.create("png", loadParams);
	    */	    

	    //scale the watermark to the appropriate size
	    overlayOp = scaleImage(overlayOp, targetWidth, targetHeight);

	    //convert to BufferedImage to operate on
	    BufferedImage overlay = overlayOp.getAsBufferedImage();
	    BufferedImage src     = source.getAsBufferedImage();

	    g = src.createGraphics();
	    g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER));
	    g.drawImage(overlay, 
			Math.max((src.getWidth()-overlay.getWidth())/2, 0), 
			Math.max((src.getHeight()-overlay.getHeight())/2, 0), 
			null);

	    ParameterBlock pb = new ParameterBlock();
	    pb.addSource( src );
	    return JAI.create("null", pb);

	} catch (IOException ioe) {                                                                                    
            log.debug("Could not load watermark image", ioe);                                                     
        } finally {
	    if (g != null) g.dispose();

	    closeStream(ss);
	}

	return source;                                                                                      
	
    }

    /**
     *
     * @param source
     * @return
     */
    public static RenderedOp stripAlphaChannel(RenderedOp source) {

	log.debug("stripAlphaChannel: " + source);

	ParameterBlock pb = new ParameterBlock(); 
	pb.addSource( source );
	pb.add( MATRIX_STRIP_ALPHA );
        
	return JAI.create("bandcombine", pb);
    }

    /**
     *
     * @param source
     * @param channel
     * @return
     */
    public static RenderedOp invertChannel(RenderedOp source, int channel) {

	log.debug("invertChannel: " + source + " channel: " + channel);

	WritableRaster raster = source.copyData();

	//create an array to hold the dest data
	int w        = raster.getWidth();
	int h        = raster.getHeight();
	int numBands = raster.getNumBands();

	if (numBands < (channel + 1)) {
	    log.warn("invertChannel() WARNING: requested channel: " + channel + 
		     " but image only has " + numBands + " bands -Skipping");
	    return source;
	}

	//iterate the pixels in the K band and invert them
	for (int x = 0; x < w; x++) { 
	    for (int y = 0; y < h; y++) {

		int val = raster.getSample(x, y, channel);
		val = 255 - val;
		raster.setSample(x, y, channel, val);
	    }
	}

	//convert raster back into renderedop
	ParameterBlock pb = new ParameterBlock();
	pb.add( new BufferedImage(source.getColorModel(), raster, false, null) );
	return JAI.create("AWTImage", pb);
    }

    /**
     *
     * @param source
     * @return
     */
    public static RenderedOp convert16BitTo8Bit(RenderedOp source) {

	log.debug("convert16BitTo8Bit: " + source );

	WritableRaster raster = source.copyData();

	//create an array to hold the dest data
	int w        = raster.getWidth();
	int h        = raster.getHeight();
	int numBands = raster.getNumBands();

	log.debug("WIDTH: " + w + " HEIGHT: " + h + " BANDS: " + numBands);

	SampleModel sm = new PixelInterleavedSampleModel(DataBuffer.TYPE_BYTE, w, h, numBands, 
							 w * numBands, //scanlineStride = width * number of bands
							 new int[] {0,1,2});
	DataBuffer db  = new DataBufferByte(w * h * numBands);
	WritableRaster output = Raster.createWritableRaster(sm, db, new java.awt.Point(0,0));

	//iterate the pixels in the K band and invert them
	for (int c = 0; c < numBands; c++) {
	    for (int x = 0; x < w; x++) { 
		for (int y = 0; y < h; y++) {
		    
		    int val = raster.getSample(x, y, c);
		    int corrected = val/256;
		    if (x %100 == 0 && y%100 == 0) log.debug("x: " + x + " y: " + y + " c: " + c + " val: " + val + " /2: " + (byte)corrected);
		    output.setSample(x, y, c, (byte)corrected);

		}
	    }
	}

	ColorModel cm = COLOR_MODEL_RGB.coerceData(output, false);

	//convert raster back into renderedop
	ParameterBlock pb = new ParameterBlock();
	pb.add( new BufferedImage(cm, output, false, null) );

	RenderedOp out = JAI.create("AWTImage", pb);
	outputImageParams(out);
	return out;
    }


    /**
     * @author Marco Jacob <mjacob@PICTURESAFE.DE>
     * 
     * @param op
     * @return RenderedOp
     */
    public static RenderedOp convertCMYKtoRGB(RenderedOp op) {

	log.debug("convertCMYKtoRGB: " + op);

	try {
	    // -- converting RGBA to CMYK
	    // Because JAI reads CMYK as RGBA this has to be transformed to
	    // CMYK first, get profile from ADOBE
	    // http://www.adobe.com/support/downloads/main.html
	    ICC_Profile cmyk_profile = ICC_Profile.getInstance(location + "/WEB-INF/profiles/USWebCoatedSWOP.icc");
	    ICC_ColorSpace cmyk_icp = new ICC_ColorSpace(cmyk_profile);
	    ColorModel cmyk_cm =
		RasterFactory.createComponentColorModel(op.getSampleModel().getDataType(),
							cmyk_icp,
							false,
							false,
							Transparency.OPAQUE);
	    // ImageLayout for RenderingHint
	    ImageLayout cmyk_il = new ImageLayout();
	    cmyk_il.setColorModel(cmyk_cm);
	    RenderingHints cmyk_hints = new RenderingHints(JAI.KEY_IMAGE_LAYOUT, cmyk_il);
	    // The convert operation itself
	    ParameterBlockJAI pb = new ParameterBlockJAI("format");
	    pb.addSource(op);
	    pb.setParameter("datatype", op.getSampleModel().getDataType());
	    op = JAI.create("format", pb, cmyk_hints);
	    
	    // -- converting CMYK to RGB
	    
	    // Create RGB ColorModel
	    ColorSpace rgb_icp = ColorSpace.getInstance(ColorSpace.CS_sRGB);
	    ColorModel rgb_cm =
		RasterFactory.createComponentColorModel(op.getSampleModel().getDataType(),
							rgb_icp,
							false,
							false,
							Transparency.OPAQUE);
	    // ImageLayout for RenderingHints
	    ImageLayout rgb_il = new ImageLayout();
	    // compatible sample model
	    rgb_il.setSampleModel(rgb_cm.createCompatibleSampleModel(op.getWidth(),
								     op.getHeight()));
	    RenderingHints rgb_hints = new RenderingHints(JAI.KEY_IMAGE_LAYOUT, rgb_il);
	    // The convert operation itself
	    pb = new ParameterBlockJAI("colorconvert");
	    pb.addSource(op);
	    pb.setParameter("colormodel", rgb_cm);
	    op = JAI.create("colorconvert", pb, rgb_hints);
	} catch(Exception ex) {
	    log.debug("", ex);
	}
	return op;
    }

    public static final double[][] MATRIX_STRIP_ALPHA = {
	{ 1.0D, 0.0D, 0.0D, 0.0D, 0.0D },
	{ 0.0D, 1.0D, 0.0D, 0.0D, 0.0D },
	{ 0.0D, 0.0D, 1.0D, 0.0D, 0.0D }
    };

    public static final double[][] MATRIX_CONVERT_16_TO_8 = {
	{ 0.5D, 0.0D, 0.0D, 0.0D },
	{ 0.0D, 0.5D, 0.0D, 0.0D },
	{ 0.0D, 0.0D, 0.5D, 0.0D }
    };

    /**
     *
     * @param source
     */
    public static void outputImageParams(RenderedOp source) {
	try {
	    log.debug("IMAGE: " + source + " WIDTH: " + source.getWidth() + " HEIGHT: " + source.getHeight());
	} catch (Exception e) {
	    log.debug("outputImageParams error", e);
	}

	// Display info about the ColorModel of the image.  
	ColorModel cm = source.getColorModel();  
	if (cm != null) {  
	    log.debug("Color model: " + cm.toString());
	    log.debug("Number of color components: " + cm.getNumComponents());  
	    log.debug("Bits per pixel: " + cm.getPixelSize());  
	    log.debug("Alpha: " + cm.hasAlpha());
	    ColorSpace cs = cm.getColorSpace();
	    if (cs != null) {
		log.debug("Color Space: " + cs.toString());
		log.debug("Type: " + cs.getType());
	    }
	    switch(cm.getTransparency()) {  
	    case Transparency.OPAQUE:      log.debug("Transparency: opaque"); break;        
	    case Transparency.BITMASK:     log.debug("Transparency: bitmask"); break;        
	    case Transparency.TRANSLUCENT: log.debug("Transparency: translucent"); break;        
	    }
	    
	} else {
	    log.debug("No color model.");  
	}   

	// Display info about the sample model
	SampleModel sm = source.getSampleModel();
	if (sm != null) {
	    log.debug("Sample model: " + sm.toString());
	    int dataType = sm.getDataType();

	    if (dataType == DataBuffer.TYPE_BYTE) {
		log.debug("Data type: byte");
	    } else if (dataType == DataBuffer.TYPE_USHORT) {
		log.debug("  Data type: short");
	    }

	    if (sm instanceof PixelInterleavedSampleModel) {
		PixelInterleavedSampleModel pism = (PixelInterleavedSampleModel)sm;
		log.debug("  pixel stride: " + pism.getPixelStride());
		log.debug("  scanline stride: " + pism.getScanlineStride());
		int[] bo = pism.getBandOffsets();
		log.debug("  band offsets: " );
		for (int b = 0; b < bo.length; b++) {
		    log.debug("    " + bo[b]);
		}
	    }

	} else {
	    log.debug("No sample model.");
	}
	
    }

    private BufferedImage toBufferedImage(Image image) {
	log.debug("toBufferedImage( " + image + " )");
        return toBufferedImage(image, image.getWidth(null), image.getHeight(null));
    }


    private BufferedImage toBufferedImage(Image image, int targetWidth, int targetHeight) {
	log.debug("toBufferedImage( " + image + ", " + targetWidth + ", " + targetHeight + " )");

	image = new ImageIcon(image).getImage();
        BufferedImage bufferedImage = new BufferedImage(targetWidth,targetHeight,BufferedImage.TYPE_INT_RGB);
 
        Graphics g = bufferedImage.createGraphics();
        g.setColor(Color.white);
        g.fillRect(0,0,targetWidth,targetHeight);
        g.drawImage(image,0,0,null);
        g.dispose();
 
        return bufferedImage;
    }
}

